﻿using System;
using System.Reflection;
using Pfz.Extensions;
using System.Collections.Generic;
using System.Diagnostics;

namespace Pfz.Factoring
{
	/// <summary>
	/// Class that deals with the creation of the appropriate factory for the given factoryResultType (or base factory type).
	/// </summary>
	public static class Factories
	{
		/// <summary>
		/// Creates a new factory prepared to create instances of the given factoryResultType.
		/// </summary>
		public static IFactories Get(Type factoryResultType)
		{
			if (factoryResultType == null)
				throw new ArgumentNullException("factoryResultType");

			Type type = typeof(Factories<>).MakeGenericType(factoryResultType);
			var field = type.GetField("Instance");
			object result = field.GetValue(null);
			return (IFactories)result;
		}
	}

	/// <summary>
	/// Class that manages the factories. In it, you can register the types that will be returned for given data-types, or create specialized 
	/// factories for datat-types.
	/// </summary>
	/// <typeparam name="T">The type of the objects that will be created by this factory.</typeparam>
	public static class Factories<T>
	{
		#region Instance
			/// <summary>
			/// Gets an Instance for this Factory. Useful only if you want to use this by interfaces.
			/// </summary>
			public static IFactories<T> Instance = new FactoryInstance();

			private sealed class FactoryInstance:
				IFactories<T>
			{
				public IFactory<T> TryCreateFactory(Type dataType)
				{
					return Factories<T>.TryCreateFactory(dataType);
				}

				public IFactory<T> CreateFactory(Type dataType)
				{
					return Factories<T>.CreateFactory(dataType);
				}

				public IFactory<T> TryCreateFactory<DataType>()
				{
					return Factories<T>.TryCreateFactory<DataType>();
				}

				public IFactory<T> CreateFactory<DataType>()
				{
					return Factories<T>.CreateFactory<DataType>();
				}

				IFactory IFactories.TryCreateFactory(Type dataType)
				{
					return Factories<T>.TryCreateFactory(dataType);
				}

				IFactory IFactories.CreateFactory(Type dataType)
				{
					return Factories<T>.CreateFactory(dataType);
				}

				IFactory IFactories.TryCreateFactory<DataType>()
				{
					return Factories<T>.TryCreateFactory<DataType>();
				}

				IFactory IFactories.CreateFactory<DataType>()
				{
					return Factories<T>.CreateFactory<DataType>();
				}
			}
		#endregion

		private static TypeDictionary<KeyValuePair<Type, ConstructorInfo>> _dictionary = new TypeDictionary<KeyValuePair<Type, ConstructorInfo>>();
		static Factories()
		{
			if (!typeof(T).ContainsCustomAttribute<FactoryBaseAttribute>())
				throw new ArgumentException("Factories<T> can only accept [FactoryBase] interfaces as generic argument.", typeof(T).FullName);

			#if WINDOWS_PHONE
			#else
			var entryAssembly = Assembly.GetEntryAssembly();
			if (entryAssembly != null)
			{
				var referencedAssemblies = entryAssembly.GetReferencedAssemblies();
				foreach(var referencedAssembly in referencedAssemblies)
					Assembly.Load(referencedAssembly);
			}
			#endif

			foreach(var assembly in AppDomain.CurrentDomain.GetAssemblies())
			{
				foreach(var type in assembly.GetTypes())
				{
					var attributes = type.GetCustomAttributes<AutoRegisterInFactoryAttributeBase>();
					foreach(var attribute in attributes)
						if (attribute.BaseFactoryType == typeof(T))
							Register(type, attribute.DataType, attribute.CanBeUsedForSubDataTypes);
				}
			}
		}


		/// <summary>
		/// Registers a type to be created for the given datatype.
		/// </summary>
		public static void Register(Type typeToCreate, Type dataType, bool canCreateForSubDataTypes=false)
		{
			if (typeToCreate == null)
				throw new ArgumentNullException("typeToCreate");

			if (dataType == null)
				throw new ArgumentNullException("dataType");

			if (!typeof(T).IsAssignableFrom(typeToCreate))
				throw new ArgumentException(typeToCreate.FullName + " must implement " + typeof(T).FullName + ".", "typeToCreate");

			var constructor = typeToCreate.GetConstructor(Type.EmptyTypes);
			if (constructor == null)
				throw new ArgumentException(typeToCreate.FullName + " does not have a public default constructor.");

			var pair = new KeyValuePair<Type, ConstructorInfo>(dataType, constructor);
			_dictionary.Set(dataType, pair, canCreateForSubDataTypes);
		}

		/// <summary>
		/// Tries to register a factory for the given dataType.
		/// This will not unregister editors for parent dataTypes capable of editing sub-types.
		/// </summary>
		public static bool Unregister(Type dataType)
		{
			return _dictionary.Remove(dataType);
		}

		/// <summary>
		/// Gets a value indicating if a factory for the given dataType can be created.
		/// </summary>
		public static bool CanCreate(Type dataType)
		{
			var constructorPair = _dictionary.FindUpOrDefault(dataType);
			return constructorPair.Value != null;
		}

		/// <summary>
		/// Tries to create a factory for the given dataType.
		/// </summary>
		public static Factory<T, DataType> TryCreateFactory<DataType>()
		{
			var constructorPair = _dictionary.FindUpOrDefault(typeof(DataType));
			var constructor = constructorPair.Value;
			if (constructor == null)
				return null;

			var realType = constructor.DeclaringType;
			if (realType.ContainsGenericParameters)
				realType = realType.MakeGenericType(typeof(DataType));

			var typedConstructor = ReflectionHelper.GetDefaultConstructorDelegate<T>(realType);
			return new Factory<T, DataType>(typedConstructor, constructorPair.Key);
		}

		private static readonly MethodInfo _method = ReflectionHelper.GetMethod(() => TryCreateFactory<int>()).GetGenericMethodDefinition();
		/// <summary>
		/// Tries to create a factory for the given dataType.
		/// </summary>
		public static IFactory<T> TryCreateFactory(Type dataType)
		{
			if (dataType == null)
				throw new ArgumentNullException("dataType");

			var method = _method.MakeGenericMethod(dataType);
			var fastMethod = method.GetDelegate();
			object result = fastMethod(null, null);
			return (IFactory<T>)result;
		}

		/// <summary>
		/// Create a factory for the given dataType or throws an exception.
		/// </summary>
		public static Factory<T, DataType> CreateFactory<DataType>()
		{
			var result = TryCreateFactory<DataType>();

			if (result == null)
				throw new ArgumentException("Can't find a factory for the given data-type.");

			return result;
		}

		/// <summary>
		/// Create a factory for the given dataType or throws an exception.
		/// </summary>
		public static IFactory<T> CreateFactory(Type dataType)
		{
			var result = TryCreateFactory(dataType);

			if (result == null)
				throw new ArgumentException("Can't find a factory for the given data-type.");

			return result;
		}
	}
}
